using System;
using System.IO;
using System.Threading;
using System.Reflection;
using System.Collections;
using System.Runtime.Serialization;

namespace Meta {
	[Serializable]
	public class Package : Namespace {
		// Used to prevent the GUI thread from blocking (or throwing exceptions) because arglist checking triggers a package load.
		[ThreadStatic()]
		public static bool inhibitLoading = false;
		string sourceFile;
		bool m_isLoaded = false;
		Thread loadingThread = null;

		object loadSyncRoot = new object[0];  // Used to interlock loading w/o locking the package itself

		public Package(object name, string sourceFile, PackageDirectory parent) : base(name) {
			this.sourceFile = sourceFile;
			this.parentDirectory = parent;
			Import(Namespace.RootNamespace);
			if (!Package.inhibitLoading)
				IsLoaded = true;   // Demand loading is cute, but it's just too confusing in some ways.
		}

		public Package(object name, string sourceFile) : base(name) {
			this.sourceFile = sourceFile;
			Import(Namespace.RootNamespace);
			if (!Package.inhibitLoading)
				IsLoaded = true;   // Demand loading is cute, but it's just too confusing in some ways.
		}

        public static NamespaceBase FindPackageFromCLRName(string name)
        {
            string[] n = name.Split(new char[] { '.' });
            switch (n.Length)
            {
                case 3:
                    return (Package)PackageDirectory.FindStandardPackageDirectory(n[1]).Lookup(Symbol.Intern(n[2]), false, true, false).value;

                case 2:
                    return PackageDirectory.FindStandardPackageDirectory(n[1]);

                default:
                    throw new Exception("Package in CLR namespace " + name + " not found");
            }
        }

		public Package (string sourceFile) : this(System.IO.Path.GetFileNameWithoutExtension(sourceFile), sourceFile) {
		}

		public bool IsLoaded {
			get {
				return m_isLoaded;
			}
			set {
				if (value && !m_isLoaded)
					MaybeLoad();
			}
		}

		public string SourceFile {
			get {
				return sourceFile;
			}
		}

		private PackageDirectory parentDirectory = null;
		public override String CLRNamespace {
			get {
				string dns = parentDirectory==null?"Meta.OrphanPackages":parentDirectory.CLRNamespace;
				return dns+"."+((Symbol)name).name;
			}
		}


		public string AuxilliaryFilePath(string filename) {
			return Path.Combine(Path.GetDirectoryName(SourceFile),filename);
		}

		public void LoadAssembly(string filename) {
			Assembly.LoadFrom(AuxilliaryFilePath(filename));
		}

		[Serializable]
		public class PackageLoadException : Exception, ISerializable {
			public readonly NamespaceBase package;

			static string PackageName(Package package) {
				object name = package.name;
				return (name is string)?((string)name):(name is Symbol)?((Symbol)name).name:"Bad package name";
			}

			public PackageLoadException(Package package, Exception innerException) : base("An exception occurred while loading the package "+PackageName(package)+": "+innerException.Message, innerException) {
				this.package = package;
			}

			protected PackageLoadException(SerializationInfo info, StreamingContext context) : base (info, context) {
				package = (NamespaceBase)info.GetValue("package", typeof(NamespaceBase));
			}

			public override void GetObjectData(SerializationInfo info, StreamingContext context) {
				base.GetObjectData(info, context);
				info.AddValue("package", package);
			}
		}

		object LoadDLL() {
			Assembly a = Assembly.LoadFrom(sourceFile);
			Loader.Load(a);
			return a;
		}

		public void MaybeLoad() {
			// Don't go into regress if we're the ones trying to load.
			if (inhibitLoading || loadingThread==Thread.CurrentThread)
				return;

			lock (loadSyncRoot) {
				if (!m_isLoaded) {
					if (TopLevel.lowLevelDebug) {
						ReallyLoad();
						loadingThread = null;
					} else {
						try {
							ReallyLoad();
						} catch (Exception e) {
							throw new PackageLoadException(this, e);
						} finally {
							loadingThread = null;
						}
					}
					m_isLoaded = true;
				}
			}
		}

		void ReallyLoad() {
			Meta.TopLevel.SetStatusMessage("Loading package "+name+"...");
			loadingThread = Thread.CurrentThread;
			EvalStack.CurrentThreadEvalStack.Push(Meta.Namespace.RootNamespace.CheckBinding("load"), new object[] { this });
//			if (String.Compare(Path.GetExtension(sourceFile), ".dll", true)==0)
//				Fluid.Bind(new object[] { Namespace.currentNamespaceFluid, this }, new Thunk(LoadDLL));
//			else
//				TopLevel.Load(FileToken.GetToken(sourceFile), this);
			TopLevel.Load(sourceFile, this);
			EvalStack.CurrentThreadEvalStack.Pop();
			Meta.TopLevel.SetStatusMessage("Loading package "+name+"... done");
		}

		public override GlobalVariable Lookup(Symbol sym, bool searchImports, bool create, bool includePrivate) {
			MaybeLoad();
			return base.Lookup(sym, searchImports, create, includePrivate);
		}
	}

	[Serializable]
	public class PackageDirectory : NamespaceBase {
		//public readonly System.Collections.Hashtable bindings = new System.Collections.Hashtable();
		public readonly string path;

		private PackageDirectory parentDirectory = null;
		public override String CLRNamespace {
			get {
				string dns = parentDirectory==null?"Meta":parentDirectory.CLRNamespace;
				return dns+"."+(string)name;
			}
		}

        // Maps directory name to PackageDirectory object
		static Hashtable packageDirectoryTable = new Hashtable();
        static Hashtable packageDirectoryNameTable = new Hashtable();

        // Finds a package directory by name
        public static PackageDirectory FindStandardPackageDirectory(string name)
        {
            return (PackageDirectory)packageDirectoryNameTable[name];
        }

        // Finds a package within a package directory by path
		public static Package Find(string path) {
			if (path==null)
				return null;

			string d = Path.GetDirectoryName(path);
			if (d==null)
				return null;
			string n = Path.GetFileNameWithoutExtension(path);
			object pd = packageDirectoryTable[d];
			if (pd!=null) {
				return (Package)((GlobalVariable)(((PackageDirectory)pd).Lookup(Symbol.Intern(n), true, true, true))).value;
			} else {
				// Crawl up one level
				n = Path.GetFileNameWithoutExtension(d);
				d = Path.GetDirectoryName(d);

				if (d==null)
					return null;
				pd = packageDirectoryTable[d];
				if (pd!=null) {
					return (Package)((GlobalVariable)(((PackageDirectory)pd).Lookup(Symbol.Intern(n), true, true, true))).value;
				} else
					// Give up
					return null;
			}
		}

		public PackageDirectory(string path) {
			bindings = new System.Collections.Hashtable();
			name = Path.GetFileName(path);
			this.path = path;
			packageDirectoryTable[path] = this;
            packageDirectoryNameTable[Path.GetFileName(path)] = this;
		}

		public override GlobalVariable Lookup(Symbol sym, bool searchImports, bool create, bool includePrivate) {
			GlobalVariable v = (GlobalVariable)bindings[sym];
			string basePath = Path.Combine(path, sym.name);

			// If it's a Directory, look for a load file within it.
			if (Directory.Exists(basePath))
				basePath = Path.Combine(basePath, sym.name);

			if (v==null) {
				string sourcePath = basePath+".meta";
				if (File.Exists(sourcePath)) {
					v = new GlobalVariable(sym, this);
					v.Define(new Package(sym, sourcePath, this), null);
					bindings[sym] = v;
					v.isConstant = true;
				} else {
					string dllPath = Path.ChangeExtension(sourcePath, ".dll");
					if (File.Exists(dllPath)) {
						v = new GlobalVariable(sym, this);
						v.Define(new Package(sym, dllPath, this), null);
						bindings[sym] = v;
						v.isConstant = true;
					} else {
						throw new FileNotFoundException("Could not find source file or DLL for package", sourcePath);
					}
				}
			}

			return v;
		}

		public override void Import(NamespaceBase import) {
			throw new Exception("Can't add imports to DirectoryPackages");
		}

		public override void Export(Symbol name) {
			throw new Exception("Can't add exports to DirectoryPackages");
		}
	}
}